package gohcl

import (
    "fmt"
    "reflect"
    "sort"
    "strings"

    "Havoc/pkg/profile/yaotl"
)

// ImpliedBodySchema produces a hcl.BodySchema derived from the type of the
// given value, which must be a struct value or a pointer to one. If an
// inappropriate value is passed, this function will panic.
//
// The second return argument indicates whether the given struct includes
// a "remain" field, and thus the returned schema is non-exhaustive.
//
// This uses the tags on the fields of the struct to discover how each
// field's value should be expressed within configuration. If an invalid
// mapping is attempted, this function will panic.
func ImpliedBodySchema(val interface{}) (schema *hcl.BodySchema, partial bool) {
    ty := reflect.TypeOf(val)

    if ty.Kind() == reflect.Ptr {
        ty = ty.Elem()
    }

    if ty.Kind() != reflect.Struct {
        panic(fmt.Sprintf("given value must be struct, not %T", val))
    }

    var attrSchemas []hcl.AttributeSchema
    var blockSchemas []hcl.BlockHeaderSchema

    tags := getFieldTags(ty)

    attrNames := make([]string, 0, len(tags.Attributes))
    for n := range tags.Attributes {
        attrNames = append(attrNames, n)
    }
    sort.Strings(attrNames)
    for _, n := range attrNames {
        idx := tags.Attributes[n]
        optional := tags.Optional[n]
        field := ty.Field(idx)

        var required bool

        switch {
        case field.Type.AssignableTo(exprType):
            // If we're decoding to hcl.Expression then absence can be
            // indicated via a null value, so we don't specify that
            // the field is required during decoding.
            required = false
        case field.Type.Kind() != reflect.Ptr && !optional:
            required = true
        default:
            required = false
        }

        attrSchemas = append(attrSchemas, hcl.AttributeSchema{
            Name:     n,
            Required: required,
        })
    }

    blockNames := make([]string, 0, len(tags.Blocks))
    for n := range tags.Blocks {
        blockNames = append(blockNames, n)
    }
    sort.Strings(blockNames)
    for _, n := range blockNames {
        idx := tags.Blocks[n]
        field := ty.Field(idx)
        fty := field.Type
        if fty.Kind() == reflect.Slice {
            fty = fty.Elem()
        }
        if fty.Kind() == reflect.Ptr {
            fty = fty.Elem()
        }
        if fty.Kind() != reflect.Struct {
            panic(fmt.Sprintf(
                "hcl 'block' tag kind cannot be applied to %s field %s: struct required", field.Type.String(), field.Name,
            ))
        }
        ftags := getFieldTags(fty)
        var labelNames []string
        if len(ftags.Labels) > 0 {
            labelNames = make([]string, len(ftags.Labels))
            for i, l := range ftags.Labels {
                labelNames[i] = l.Name
            }
        }

        blockSchemas = append(blockSchemas, hcl.BlockHeaderSchema{
            Type:       n,
            LabelNames: labelNames,
        })
    }

    partial = tags.Remain != nil
    schema = &hcl.BodySchema{
        Attributes: attrSchemas,
        Blocks:     blockSchemas,
    }
    return schema, partial
}

type fieldTags struct {
    Attributes map[string]int
    Blocks     map[string]int
    Labels     []labelField
    Remain     *int
    Body       *int
    Optional   map[string]bool
}

type labelField struct {
    FieldIndex int
    Name       string
}

func getFieldTags(ty reflect.Type) *fieldTags {
    ret := &fieldTags{
        Attributes: map[string]int{},
        Blocks:     map[string]int{},
        Optional:   map[string]bool{},
    }

    ct := ty.NumField()
    for i := 0; i < ct; i++ {
        field := ty.Field(i)
        tag := field.Tag.Get("yaotl")
        if tag == "" {
            continue
        }

        comma := strings.Index(tag, ",")
        var name, kind string
        if comma != -1 {
            name = tag[:comma]
            kind = tag[comma+1:]
        } else {
            name = tag
            kind = "attr"
        }

        switch kind {
        case "attr":
            ret.Attributes[name] = i
        case "block":
            ret.Blocks[name] = i
        case "label":
            ret.Labels = append(ret.Labels, labelField{
                FieldIndex: i,
                Name:       name,
            })
        case "remain":
            if ret.Remain != nil {
                panic("only one 'remain' tag is permitted")
            }
            idx := i // copy, because this loop will continue assigning to i
            ret.Remain = &idx
        case "body":
            if ret.Body != nil {
                panic("only one 'body' tag is permitted")
            }
            idx := i // copy, because this loop will continue assigning to i
            ret.Body = &idx
        case "optional":
            ret.Attributes[name] = i
            ret.Optional[name] = true
        default:
            panic(fmt.Sprintf("invalid hcl field tag kind %q on %s %q", kind, field.Type.String(), field.Name))
        }
    }

    return ret
}
